﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;

namespace SmartMathLibrary.Interfaces.Web.Twitter
{
    /// <summary>
    /// Describes the function, which will be invoked if an error occurs. 
    /// </summary>
    public delegate void PrintError(String value);

    /// <summary>
    /// Describes the function, which will be invoked if the crawler prints a messages. 
    /// </summary>
    public delegate void PrintMessage(String value);

    /// <summary>
    /// This method describes an find user event, which will be invoked if the crawler finds a twitter user. 
    /// </summary>
    public delegate void FindUser(String value);

    /// <summary>
    /// This class represents a crawler for twitter users. It starts at a specified twitter account and grabs all following 
    /// accounts up to a specified level.
    /// </summary>
    [Serializable]
    public class UserCrawler
    {
        /// <summary>
        /// This field holds the maximum level to scan.
        /// </summary>
        private readonly int maxLevel;

        /// <summary>
        /// This field holds the current scan level.
        /// </summary>
        private int currentLevel;

        /// <summary>
        /// This field holds the user names, which the crawler found.
        /// </summary>
        private readonly List<String> usernames;

        /// <summary>
        /// This field holds the print error function.
        /// </summary>
        private PrintError printErrorFunction;

        /// <summary>
        /// This field holds the print error function.
        /// </summary>
        private PrintMessage printMessageFunction;

        /// <summary>
        /// This field holds the user find event function.
        /// </summary>
        private FindUser findUserFunction;

        /// <summary>
        /// This field holds the twitter root url.
        /// </summary>
        private const String twitterURL = "http://twitter.com/";

        /// <summary>
        /// Initializes a new instance of the <see cref="UserCrawler"/> class.
        /// </summary>
        /// <param name="maxLevel">The maximum level to scan.</param>
        /// <param name="findUserFunction">The FindUserEvent function for the crawler.</param>
        /// <param name="printMessageFunction">The PrintMessageFunction for the crawler.</param>
        /// <param name="printErrorFunction">The PrintErrorFunction for the crawler.</param>
        public UserCrawler(int maxLevel, FindUser findUserFunction, PrintMessage printMessageFunction,
                           PrintError printErrorFunction)
        {
            this.maxLevel = maxLevel;
            this.usernames = new List<string>();
            this.findUserFunction = findUserFunction;
            this.printErrorFunction = printErrorFunction;
            this.printMessageFunction = printMessageFunction;
        }

        /// <summary>
        /// Gets the usernames, which were found by the crawler.
        /// </summary>
        /// <value>The usernames, which were found by the crawler.</value>
        public List<string> Usernames
        {
            get { return usernames; }
        }

        /// <summary>
        /// Gets or sets the PrintErrorFunction for the crawler.
        /// </summary>
        /// <value>The PrintErrorFunction for the crawler.</value>
        public PrintError PrintErrorFunction
        {
            get { return printErrorFunction; }
            set { printErrorFunction = value; }
        }

        /// <summary>
        /// Gets or sets the FindUserEvent function for the crawler.
        /// </summary>
        /// <value>The FindUserEvent function.</value>
        public FindUser FindUserFunction
        {
            get { return findUserFunction; }
            set { findUserFunction = value; }
        }

        /// <summary>
        /// Gets or sets the PrintMessageFunction for the crawler.
        /// </summary>
        /// <value>The PrintMessageFunction for the crawler.</value>
        public PrintMessage PrintMessageFunction
        {
            get { return printMessageFunction; }
            set { printMessageFunction = value; }
        }

        /// <summary>
        /// Starts the crawler.
        /// </summary>
        /// <param name="startUser">The start user for the crawler.</param>
        public void StartCrawler(String startUser)
        {
            this.ScanFollowers(startUser);
        }

        private void ScanFollowers(String screenname)
        {
            this.currentLevel++;

            if (this.printMessageFunction != null)
            {
                this.printMessageFunction("Scan:" + screenname + "@Level:" + currentLevel);
            }

            int tries = 0;
            Uri startURL = new Uri(twitterURL + screenname + "/");
            String grabResult = this.DownloadPage(startURL, 5000);

            while ((grabResult == null) && (tries < 5))
            {
                grabResult = this.DownloadPage(startURL, 5000);
                tries++;
            }

            if (tries >= 5)
            {
                if (this.printMessageFunction != null)
                {
                    this.printErrorFunction("Cannot access profile " + screenname);
                }

                this.currentLevel--;

                return;
            }

            List<String> users = this.GetUsers(this.GetAddressTags(grabResult));

            if ((users.Count == 0) && (tries < 5))
            {
                this.currentLevel--;

                if (this.printMessageFunction != null)
                {
                    this.printErrorFunction("Scan:" + screenname);
                }

                return;
            }

            for (int i = 0; i < users.Count; i++)
            {
                this.usernames.Add(users[i]);

                if (this.findUserFunction != null)
                {
                    this.findUserFunction(users[i]);
                }

                if (currentLevel <= this.maxLevel)
                {
                    this.ScanFollowers(users[i]);
                }
            }

            this.currentLevel--;
        }

        private List<String> GetUsers(List<String> tags)
        {
            List<String> result = new List<String>();

            foreach (String tag in tags)
            {
                String startTag = "href=\"/";
                int start = tag.IndexOf(startTag);

                if (start > 0)
                {
                    start += startTag.Length;
                    int length = tag.IndexOf("\"", start) - start;
                    String user = tag.Substring(start, length);

                    result.Add(user);
                }
            }

            return result;
        }

        private List<String> GetAddressTags(String data)
        {
            List<String> result = new List<String>();

            while (true)
            {
                int start = data.IndexOf("<span class=\"vcard\">");

                if (start < 0)
                {
                    break;
                }

                int length = data.IndexOf("</span>", start) - start;

                result.Add(data.Substring(start, length));
                data = data.Substring(start + length);
            }

            return result;
        }

        private String DownloadPage(Uri url, int timeout)
        {
            try
            {
                WebRequest http = HttpWebRequest.Create(url);
                http.Timeout = timeout;
                HttpWebResponse response = (HttpWebResponse) http.GetResponse();
                StreamReader stream = new StreamReader(response.GetResponseStream(), Encoding.ASCII);

                String result = stream.ReadToEnd();

                response.Close();
                stream.Close();

                return result;
            }
            catch (Exception)
            {
                return null;
            }
        }
    }
}